[Feature]: Implement B-spline SVG path component for workflow links #15#16
[Feature]: Implement B-spline SVG path component for workflow links #15#16LudoBroche wants to merge 6 commits into
Conversation
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
|
Since, the scope of this PR is not to compute the links coordinates from an ewoks workflows. I created a a small example script to show the API in examples/cubic_bezier_link.py : Path definition using directly the bezier control points: letter_w = CubicBezierPath(
start=(135, 84),
segments=[
CubicBezierSegment((142, 124), (145, 154), (158, 156)),
CubicBezierSegment((169, 158), (176, 102), (187, 102)),
CubicBezierSegment((198, 102), (196, 157), (210, 157)),
CubicBezierSegment((224, 157), (236, 105), (244, 84)),
],
)Path definition using polyline coordinates and corner radius : underline = CubicBezierPath.from_points(
points=[
(40, 175),
(240, 175),
(240, 205),
(480, 205),
(480, 25),
(320, 25),
(320, 205),
],
radius=30,
)Svg Path element create from the CubicBezierPath object : svg_w = SvgLinkCubicBezier(
letter_w,
color="#00c2a8",
stroke_width=4,
stroke_dash="5 10",
)
svg_underline = SvgLinkCubicBezier(
underline,
color="#ff00aa",
stroke_width=3,
stroke_dash="12 6",
),Add elements to the canvas and draw: canvas = SvgCanvas(width=520, height=220)
canvas.add_element(svg_w)
canvas.add_element(svg_underline)
canvas.draw(output_path)Of course it's compatible with groups : links_group = SvgGroup()
links_group.add_elements([svg_w, svg_underline])
links_group.translate(5, 5)
canvas.add_element(links_group)
canvas.draw(output_path)Voila : |
| @@ -0,0 +1,3 @@ | |||
| from .cubic_bezier_path import CubicBezierPath # noqa: F401 | |||
| from .cubic_bezier_path import CubicBezierSegment # noqa: F401 | |||
| from .cubic_bezier_path import Point # noqa: F401 | |||
There was a problem hiding this comment.
I would not export the members of cubic_bezier_path here since this adds maintenance for not much.
Members can be imported by users and consumer modules from their original location.
There was a problem hiding this comment.
Removed all init.py imports
| if tag not in ("rect", "circle", "text", "line"): | ||
| if tag not in ("rect", "circle", "text", "line", "path"): | ||
| raise ValueError( | ||
| f"Invalid SVG tag: {tag}. Supported tags are 'rect', 'circle', 'text'," |
There was a problem hiding this comment.
| f"Invalid SVG tag: {tag}. Supported tags are 'rect', 'circle', 'text'," | |
| f"Invalid SVG tag: {tag}. Supported tags are 'rect', 'circle', 'text',"path", " |
I would suggest to make a constant out of the supported tags to avoid this issue in the future.
| """ | ||
|
|
||
| if self._tag == "path": | ||
| return |
There was a problem hiding this comment.
Should we print a warning there?
There was a problem hiding this comment.
Absolutely
if self._tag == "path":
warnings.warn(
"set_position() has no effect on 'path' elements; position is "
"defined by the path data instead.",
stacklevel=2,
)
return| def test_stroke_width_formatted_without_trailing_zero(): | ||
| link = SvgLinkCubicBezier(SIMPLE_PATH, stroke_width=4.0) | ||
| assert link.get_attr("style") == "stroke-width:4" |
There was a problem hiding this comment.
What is the point of this test? Is "stroke-width:4.0" not valid SVG?
There was a problem hiding this comment.
You right I wanted to reach that line of code.
I can do it in the previous test since it's just a consmetic formating.
| (None, None, None, None), | ||
| ], | ||
| ) | ||
| def test_style_attribute_combinations(color, stroke_width, stroke_dash, expected_style): |
There was a problem hiding this comment.
The test is so simple that it could be made more readable by doing the combinations one after the other rather than using parametrize.
There was a problem hiding this comment.
Done split the tests in the different style calls.
| @@ -0,0 +1,98 @@ | |||
| from dataclasses import dataclass | |||
| from typing import Self | |||
There was a problem hiding this comment.
| from typing import Self | |
| import sys | |
| if sys.version_info < (3, 11): | |
| from typing_extensions import Self | |
| else: | |
| from typing import Self |
| return (1 if end[0] > start[0] else -1, 0) | ||
|
|
||
|
|
||
| def _distance(start: Point, end: Point) -> float: |
There was a problem hiding this comment.
This distance is computed with the L1 norm, not the usual L2. Is this normal?
| """ | ||
|
|
||
| def __init__(self, width: int, height: int): | ||
| def __init__(self, width: int | float, height: int | float): |
There was a problem hiding this comment.
We can use numbers.Number instead: https://docs.python.org/3.10/library/numbers.html#numbers.Number
| from xml.etree.ElementTree import Element | ||
|
|
||
| SvgTag = Literal["rect", "circle", "text", "line", "path"] | ||
| SUPPORTED_TAGS: Final[tuple[SvgTag, ...]] = get_args(SvgTag) |
There was a problem hiding this comment.
Didn't know about get_args, pretty sweet.
Final is probably overkill since it does not raise an error at runtime and we don't run type checking.
| path: CubicBezierPath, | ||
| color: str | None = None, | ||
| stroke_width: float | None = None, | ||
| stroke_dash: str | None = None, |
There was a problem hiding this comment.
| stroke_dash: str | None = None, | |
| stroke_dasharray: str | None = None, |
Might as well use the same name as the SVG property 🤷
Description
Closes #15 This PR implements the isolated Bezier SVG component.
Changes Made
SvgLinkCubicBezierclass, to generate an svg element from aCubicBezierPathdata structure and the desired style properties (color, stroke_width ...)CubicBezierPathis a new data class representing the start, end and control points of the bezier path. A helper function (from_points(points, radius)) translate a polyline coordinates into rounded cubic bezier path.Note to Reviewer(s)
This PR is strictly limited to the SVG path generation. It does not include the ewoks JSON parsing, collision avoidance, routing calculations, or node rendering.